與整數的二進制表達相同我們可以假設任意小數的二進制為 1011.0011也就是說,按照跟整數轉換相同的思路我們可以換算出
1*2^(3) + 0*2^(2) + 1*2^(1) +1*2^(0) + 0*2^(-1) + 0*2^(-2) + 1*2^(-3) + 1*2^(4) = 11.1875
不過像1011.0011這種表達式是給人看得,真正在計算機內傳輸,是運用更高效率的科學記號方式,更進一步說是運用了正則表達式與ECXESS系統,畢竟使用上述二進制表示方法,不僅沒有告訴計算機小數點前後各有幾位,在傳統32bits的浮點數下能表示的範圍也有限,所以該方法僅限於讓人們清楚意識到,小數位跟整數位一樣,是使用2的次方數作為進位依據,差別僅在於所使用為負數
我們從上一張圖可以看出一個問題,那就是小數位的表達是存在一個特定誤差範圍的,例如若要表示0.1(10進制),但是1* 2^(-1) = 0.5、1* 2^(-2) = 0.25、1* 2^(-3) = 0.125、1* 2^(-4) = 0.0625...
不管我們怎麼分配始終無法完美取得0.1,這也是計算機在小數位上存在的既定誤差,另一方面0.1轉換成二進制會等於0.0001100110011...無限循環,永遠無法求得準確值,計算機最後只能想出折衷方案取四捨五入或直接取到固定位數
浮點數表達式是計算機內用來表示小數的方法,由IEEE規定組成格式,並用有32 bits單精度(float)與64 bits雙精度(double)的資料型態
浮點數的表達方法主要由4個參數組成,分別是正負符號、尾數(m)、基數(n)、指數(e) :
那麼問題來了,二進制的浮點數該怎麼用上述科學記號方式表達成二進位的0和1進而讓計算機能夠讀懂? 換個角度想,我們把數字改成10進制,我們先就整數而言,比如說12500 :
10進位整數位中符號為正,尾數為1.25、基數為10、指數為4,這種方式是科學記號約定俗成的表達方式,其中重點在尾數應該表示為一個大於等於1,小於10的一組小數,因為是10進位,每增加一個位就必須乘以10,所以基數應當表達為10,指數表達進位數
要將12500轉換成浮點表達事前必須先概括介紹使用正則表達式與Excess系統的目的
正則表達式
計算機系統中用來表示浮點數尾數的方法
Excess系統
計算機系統中用來表示指數的的方法
使用這種類似科學記號的表達方式有效增加能容納位數,以及更便於計算機進行運算,因為最終目的是要使計算機能夠讀懂12500,更精確地說就是0和1的組合
可能很多人會直接將12500轉換成二進制 = 0011 0000 1101 0100,作為運算結果,不過浮點數的表達方式與整數是完全不同的,所以這種轉換是錯誤的,我們來看一下IEEE是怎麼規定浮點數的二進位表達方式 :
先釐清最簡單的部分,舉float為例,12500為正數,所以符號位是0,計算機是使用二進制單位表示,所以基數為2
指數我們需要使用Excess系統,該系統會將例如8 bits的指數位(0~255)取中間值作為0,也就是說127
0111 1111代表指數為0,二進制的127~255依序分別是0、1、2 ... 128,相反的二進制的126~0分別對應-1、-2 ... -127
12500存在於2^(13) = 81922到2^(14)=16384之間,所以我們求得12500的指數為13,因為使用Excess系統故加上127=140,再將140轉換成二進制可以得到1000 1100,這就是8位指數位
而正則表達式就像上面的小數轉換表一樣將賦值成1的位數呈上2^(-n)次方然後依序相加,需要注意最後需要加上1 * 2^(0),這是IEEE規定的浮點數格式,你可以想像科學記號也對於尾數的規定,在浮點數中我們使用的是小數點前固定為1的正則表達式,目的是湊出一個大於等於1小於2的一個小數位與指數位相乘進而得出近似值
因為+1是一個約定俗成,人盡皆知的概念,所以在真正填充二進位值的時候可以省略掉1這個欄位,空出來的空位剛好可以填充多一個小數位增加精度(雖然也不會增加多少,畢竟都乘到2^(-23))
回到12500的例子,轉成二進制等於 011 0000 1101 0100,經過右移將小數點前一位製作成1,最後在將小數點後捕到23位,該值就是12500的浮點數尾數,但是因為浮點數的正則表達規範,我們可以省略掉小數位前的1
不過在真實的案例中我們不太需要去編寫API去刻意轉換浮點數的二進位表達,因為當我們在宣告一個浮點變數時,計算機會自動將指標指向內存4 bytes區塊,並給定使用浮點表達式所生成的二進制碼資料初始值
程式案例
void float_to_hex(float a){
float num = a;
char buffer[40] = {0};
unsigned int get_hex;
int i,j=0;
get_hex = *(unsigned int*)&a; // hex必須要用unsigned int來取值
printf("hex: %0x\n", get_hex);
for(i = 0; i < 32; i++){
if(i == 1 || i == 9){
buffer[j++] = '-';
}
if((get_hex >> (32-(i+1)))%2 == 1)
buffer[j++] = '1';
else
buffer[j++] = '0';
}
buffer[j] = '\0';
printf("bin: %s\n", buffer);
return;
}
執行結果
在前文我們提到計算機在做浮點運算時,會產生些微誤差,比如說0.1連續加10次,理論上應該要為10,但是永遠不可能
float a=0;
for(int i = 1 ; i <= 10 ; i++){
a += 0.1
另外一種誤差錯誤發生在利用浮點作為判斷依據,這種寫法很危險,假如小數位發生錯誤,整個程式將會卡在無線迴圈中,所以一般我們不會使用浮點數作為判斷式的判斷子
float a=0;
while(1){
a += 0.02;
printf("%f\n", a);
if(a == 4) // loop
break;
}
要解決浮點數造成的問題,可以試著把浮點數轉成整數來計算,不是直接casting,而是想辦法藉由運算消除小數部分(例如乘以1000後運算),運算出結果後再將結果轉成浮點數,或者因為浮點造成的誤差實際上很小,如果對誤差容忍度不會太低,那大可忽略不計,例如室外測量溫度範圍在0~40度,那麼0.00001的誤差實際上並不影響溫度計運作
// convert float to str
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
float sqrt_float(uint16_t num, int exp){
float number = num;
if(exp == 0)
return 1;
else{
for(int i = 1 ; i < abs(exp) ; i++)
number *= num;
}
if(exp < 0)
number = 1/number;
return number;
}
uint32_t float_to_hex(float a){
float num = a;
char buffer[40] = {0};
unsigned int get_hex;
int i,j=0;
get_hex = *(unsigned int*)&a; // hex必須要用unsigned int來取值
printf("hex: %0x\n", get_hex);
printf("dec: %0u\n", get_hex);
for(i = 0; i < 32; i++){
if(i == 1 || i == 9){
buffer[j++] = '-';
}
if((get_hex >> (32-(i+1)))%2 == 1)
buffer[j++] = '1';
else
buffer[j++] = '0';
}
buffer[j] = '\0';
printf("bin: %s\n", buffer);
return get_hex;
}
char *hex_to_float_to_str(uint32_t num, int8_t precision){ // 16進制轉浮點再轉字串
int j=0, exp, m;
float real_part, remain_part, n, value;
bool negative=false;
char *buffer = (char*)malloc(128);
if((num & 0x80000000) != 0){
negative = true; // 正負號
buffer[j++] = '-';
}
exp = ((num & 0x7f800000) >> 23) - 127; // 取指數
m = ((num & 0x007fffff));
for(uint16_t i = 1 ; i <= 23 ; i++) // 取尾數
n += ((m >> (23 - i))%2) * (1/sqrt_float(2,i));
n++;
value = n*(sqrt_float(2,exp));
// Float to string, since no function in C.
real_part = floor(value);
remain_part = value - real_part;
for(uint16_t i = 1 ; i <= precision ; i++)
remain_part *= 10;
remain_part = floor(remain_part);
if(remain_part < 0) // block '-'
remain_part *= -1;
sprintf(buffer+j, "%.0f.%.0f", real_part, remain_part);
return buffer;
}
int main(){
float a;
uint32_t test;
char *buffer_p;
printf("Input a float type number: ");
scanf("%f", &a);
test = float_to_hex(a); // float to bin & hex
buffer_p = hex_to_float_to_str(test, 5); // turn hex to float
printf("float to string: %s\n", buffer_p);
return 0;
}